/*
 * Copyright 1999-2011 Alibaba Group.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ezcloud.engine.rpc.asm;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Proxy.
 *
 * @author qian.lei
 */

public abstract class Proxy {
	private static final AtomicLong PROXY_CLASS_COUNTER = new AtomicLong(0);

	private static final String PACKAGE_NAME = Proxy.class.getPackage().getName();

	private static final ConcurrentMap<ClassLoader, ProxyXInstanceCache> ProxyCacheMap = new ConcurrentHashMap<>();

	private static final Object PendingGenerationMarker = new Object();

	private static class ProxyXInstanceCache {
		private Lock lock = null;
		private Condition waitLoad = null;
		private ConcurrentMap<String, Object> cacheClass = null;

		public ProxyXInstanceCache() {
			this.lock = new ReentrantLock();
			this.waitLoad = this.lock.newCondition();
			this.cacheClass = new ConcurrentHashMap<>();
		}

		public Proxy getProxy(String key, ClassLoader cl, Class<?>... ics) {
			do {
				Object v = cacheClass.get(key);
				if (v instanceof Reference<?>) {
					v = ((Reference) v).get();
					if (v != null)
						return (Proxy) v;
				}

				lock.lock();
				try {
					if (v == PendingGenerationMarker) {
						// wait other thread load classNames;
						try {
							this.waitLoad.await();
						} catch (InterruptedException e) {
							// ignore e;
						}
					} else {
						// my load ... classNames.
						cacheClass.put(key, PendingGenerationMarker);
						// 开始 load .... .
						Proxy vi = null;
						try {
							vi = newProxy(cl, ics);
						} finally {
							if (vi == null) {
								cacheClass.remove(key, PendingGenerationMarker);
							} else {
								cacheClass.put(key, new WeakReference<>(vi));
							}
							// notifyAll ...
							this.waitLoad.signalAll();
						}
					}
				} finally {
					lock.unlock();
				}
			} while (true);
		}
	}

	/**
	 * Get proxy.
	 *
	 * @param ics interface class array.
	 * @return Proxy instance.
	 */
	public static Proxy getProxy(Class<?>... ics) {
		return getProxy(ClassHelper.getCallerClassLoader(Proxy.class), ics);
	}

	/**
	 * Get proxy.
	 *
	 * @param cl  class loader.
	 * @param ics interface class array.
	 * @return Proxy instance.
	 */
	public static Proxy getProxy(ClassLoader cl, Class<?>... ics) {
		String key = getKey(cl, ics);

		// get cache by class loader.
		ProxyXInstanceCache cache = ProxyCacheMap.getOrDefault(cl, new ProxyXInstanceCache());
		return cache.getProxy(key, cl, ics);
	}

	private static Proxy newProxy(ClassLoader cl, Class<?>... ics) throws RuntimeException {
		long id = PROXY_CLASS_COUNTER.getAndIncrement();
		String pkg = null;
		ClassGenerator ccp = null, ccm = null;
		try {
			ccp = ClassGenerator.newInstance(cl);

			Set<String> worked = new HashSet<String>();
			List<Method> methods = new ArrayList<Method>();

			for (int i = 0; i < ics.length; i++) {
				if (!Modifier.isPublic(ics[i].getModifiers())) {
					String npkg = ics[i].getPackage().getName();
					if (pkg == null) {
						pkg = npkg;
					} else {
						if (!pkg.equals(npkg))
							throw new IllegalArgumentException("non-public interfaces from different packages");
					}
				}
				ccp.addInterface(ics[i]);

				for (Method method : ics[i].getMethods()) {
					String desc = ReflectUtils.getDesc(method);
					if (worked.contains(desc))
						continue;
					worked.add(desc);

					int ix = methods.size();
					Class<?> rt = method.getReturnType();
					Class<?>[] pts = method.getParameterTypes();

					StringBuilder code = new StringBuilder("Object[] args = new Object[").append(pts.length).append("];");
					for (int j = 0; j < pts.length; j++)
						code.append(" args[").append(j).append("] = ($w)$").append(j + 1).append(";");
					code.append(" Object ret = handler.invoke(this, methods[" + ix + "], args);");
					if (!Void.TYPE.equals(rt))
						code.append(" return ").append(asArgument(rt, "ret")).append(";");

					methods.add(method);
					ccp.addMethod(method.getName(), method.getModifiers(), rt, pts, method.getExceptionTypes(), code.toString());
				}
			}

			if (pkg == null)
				pkg = PACKAGE_NAME;

			// create ProxyInstance class.
			String pcn = pkg + ".proxy" + id;
			ccp.setClassName(pcn);
			ccp.addField("public static java.lang.reflect.Method[] methods;");
			ccp.addField("private " + InvocationHandler.class.getName() + " handler;");
			ccp.addConstructor(Modifier.PUBLIC, new Class<?>[]{InvocationHandler.class}, new Class<?>[0], "handler=$1;");
			ccp.addDefaultConstructor();
			Class<?> clazz = ccp.toClass();
			clazz.getField("methods").set(null, methods.toArray(new Method[0]));

			// create Proxy class.
			String fcn = Proxy.class.getName() + id;
			ccm = ClassGenerator.newInstance(cl);
			ccm.setClassName(fcn);
			ccm.addDefaultConstructor();
			ccm.setSuperClass(Proxy.class);
			ccm.addMethod("public Object newInstance(" + InvocationHandler.class.getName() + " h){ return new " + pcn + "($1); }");
			Class<?> pc = ccm.toClass();
			return (Proxy) pc.newInstance();
		} catch (RuntimeException e) {
			throw e;
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		} finally {
			// release ClassGenerator
			if (ccp != null)
				ccp.release();
			if (ccm != null)
				ccm.release();
		}
	}

	private static String getKey(ClassLoader cl, Class<?>[] ics) {
		if (ics.length > 65535)
			throw new IllegalArgumentException("interface limit exceeded");

		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < ics.length; i++) {
			String itf = ics[i].getName();
			if (!ics[i].isInterface())
				throw new RuntimeException(itf + " is not a interface.");

			Class<?> tmp = null;
			try {
				tmp = Class.forName(itf, false, cl);
			} catch (ClassNotFoundException e) {
			}

			if (tmp != ics[i])
				throw new IllegalArgumentException(ics[i] + " is not visible from class loader");

			sb.append(itf).append(';');
		}

		// use interface class name list as key.
		return sb.toString();
	}

	/**
	 * get instance with special handler.
	 *
	 * @return instance.
	 */
	abstract public Object newInstance(InvocationHandler handler);

	protected Proxy() {
	}

	private static String asArgument(Class<?> cl, String name) {
		if (cl.isPrimitive()) {
			if (Boolean.TYPE == cl)
				return name + "==null?false:((Boolean)" + name + ").booleanValue()";
			if (Byte.TYPE == cl)
				return name + "==null?(byte)0:((Byte)" + name + ").byteValue()";
			if (Character.TYPE == cl)
				return name + "==null?(char)0:((Character)" + name + ").charValue()";
			if (Double.TYPE == cl)
				return name + "==null?(double)0:((Double)" + name + ").doubleValue()";
			if (Float.TYPE == cl)
				return name + "==null?(float)0:((Float)" + name + ").floatValue()";
			if (Integer.TYPE == cl)
				return name + "==null?(int)0:((Integer)" + name + ").intValue()";
			if (Long.TYPE == cl)
				return name + "==null?(long)0:((Long)" + name + ").longValue()";
			if (Short.TYPE == cl)
				return name + "==null?(short)0:((Short)" + name + ").shortValue()";
			throw new RuntimeException(name + " is unknown primitive type.");
		}
		return "(" + ReflectUtils.getName(cl) + ")" + name;
	}
}